/*
 * Created on Sep 5, 2005
 */
package bancosys.tec.persist.bussobj;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Transient;

import jmine.tec.persist.PersistMessages;
import jmine.tec.utils.spring.BeanReference;
import jmine.tec.utils.spring.SpringBeanReference;

import org.apache.log4j.Logger;
import org.hibernate.proxy.HibernateProxyHelper;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.controller.PersistenceEnabledController;
import bancosys.tec.persist.exception.PersistenceException;
import bancosys.tec.persist.persister.Persister;

/**
 * @author Frank Cara
 */
@MappedSuperclass
public abstract class PersistableBusinessObject implements Persistable, Serializable {
    
    private static final int IS_SIZE = 2;

    private static final int GET_SIZE = 3;

    private BeanReference<PersistenceEnabledController> controller;

    private transient boolean dirty = false;

    private static Map<Class<? extends PersistableBusinessObject>, Method> getterCache =
            new ConcurrentHashMap<Class<? extends PersistableBusinessObject>, Method>();

    private static Map<Class<? extends PersistableBusinessObject>, Method> setterCache =
            new ConcurrentHashMap<Class<? extends PersistableBusinessObject>, Method>();

    private static Map<Class<? extends PersistableBusinessObject>, Field> fieldCache =
            new ConcurrentHashMap<Class<? extends PersistableBusinessObject>, Field>();

    /**
     * Devolve o persister desse bean.
     * 
     * @return o persister desse bean.
     */
    @Transient
    public Persister<? extends Persistable> getPersister() {
        return this.getController().getPersisterFactory().getPersister(this.getController().getSessionFactory(), this);
    }

    /**
     * Devolve o persister puro desse bean.
     * 
     * @return o persister puro desse bean.
     */
    @Transient
    public Persister<? extends Persistable> getPurePersister() {
        return this.getController().getPersisterFactory().getPurePersister(this.getController().getSessionFactory(), this);
    }

    /**
     * Devolve a chave primária.
     * 
     * @return a chave primária.
     */
    @Transient
    public Serializable getPk() {
        try {
            Method method = this.findPkGetter();
            if (method != null) {
                return (Serializable) method.invoke(this, (Object[]) null);
            }
            Field field = this.findPkField();
            if (field != null) {
                if (field.isAccessible()) {
                    return (Serializable) field.get(this);
                } else {
                    field.setAccessible(true);
                    Serializable pk = (Serializable) field.get(this);
                    field.setAccessible(false);
                    return pk;
                }
            }
        } catch (IllegalArgumentException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_GET_ID.create(this.getClass()), e);
        } catch (IllegalAccessException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_GET_ID.create(this.getClass()), e);
        } catch (InvocationTargetException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_GET_ID.create(this.getClass()), e);
        } catch (ClassCastException e) {
            throw new PersistenceException(PersistMessages.ID_NOT_SERIALIZABLE.create(this.getClass()), e);
        }
        throw new PersistenceException(PersistMessages.ID_NOT_FOUND.create(this.getClass()));
    }

    /**
     * Define a chave primária.
     * 
     * @param pk a chave primária.
     */
    public void setPk(Serializable pk) {
        try {
            Method method = this.findPkSetter();
            if (method != null) {
                method.invoke(this, pk);
                return;
            }
            Field field = this.findPkField();
            if (field != null) {
                if (field.isAccessible()) {
                    field.set(this, pk);
                } else {
                    field.setAccessible(true);
                    field.set(this, pk);
                    field.setAccessible(false);
                }
                return;
            }
        } catch (IllegalArgumentException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_GET_ID.create(this.getClass()), e);
        } catch (IllegalAccessException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_GET_ID.create(this.getClass()), e);
        } catch (InvocationTargetException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_GET_ID.create(this.getClass()), e);
        } catch (ClassCastException e) {
            throw new PersistenceException(PersistMessages.ID_NOT_SERIALIZABLE.create(this.getClass()), e);
        }
        throw new PersistenceException(PersistMessages.ID_NOT_FOUND.create(this.getClass()));
    }

    /**
     * Devolve o campo que contém a chave primária.
     * 
     * @return o campo que contém a chave primária.
     */
    private Field findPkField() {
        if (fieldCache.containsKey(this.getClass())) {
            return fieldCache.get(this.getClass());
        }

        Field pkField = null;
        for (Class<?> clazz = this.getClass(); !clazz.equals(Object.class) && pkField == null; clazz = clazz.getSuperclass()) {
            Field[] fields = clazz.getDeclaredFields();

            for (Field field : fields) {
                Id id = field.getAnnotation(Id.class);
                if (id != null) {
                    pkField = field;
                    break;
                }
            }
        }
        if (pkField != null) {
            fieldCache.put(this.getClass(), pkField);
        }
        return pkField;
    }

    /**
     * Devolve o método que retorna a chave primária.
     * 
     * @return o método que retorna a chave primária.
     */
    private Method findPkGetter() {
        if (getterCache.containsKey(this.getClass())) {
            return getterCache.get(this.getClass());
        }

        Method getter = null;
        Method[] methods = this.getClass().getMethods();
        for (Method method : methods) {
            Id id = method.getAnnotation(Id.class);
            if (id != null) {
                getter = method;
                break;
            }
        }
        if (getter != null) {
            getterCache.put(this.getClass(), getter);
        }
        return getter;
    }

    /**
     * Devolve o método que define a chave primária.
     * 
     * @return o método que define a chave primária.
     */
    private Method findPkSetter() {
        if (setterCache.containsKey(this.getClass())) {
            return setterCache.get(this.getClass());
        }
        Method getter = this.findPkGetter();
        if (getter == null) {
            return null;
        }
        Class<?> returnType = getter.getReturnType();

        String getterName = getter.getName();
        String setterName = null;
        if (getterName.startsWith("is")) {
            setterName = "set" + getterName.substring(IS_SIZE);
        }
        if (getterName.startsWith("get")) {
            setterName = "set" + getterName.substring(GET_SIZE);
        }
        try {
            Method method = this.getClass().getMethod(setterName, new Class[]{ returnType });
            if (method != null) {
                setterCache.put(this.getClass(), method);
            }
            return method;
        } catch (SecurityException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_GET_ID.create(this.getClass()), e);
        } catch (NoSuchMethodException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_GET_ID.create(this.getClass()), e);
        }
    }

    /**
     * Devolve o controller do bean.
     * 
     * @return o controller do bean.
     */
    @Transient
    public PersistenceEnabledController getController() {
        return this.controller == null ? null : this.controller.get();
    }

    /**
     * Define o controller.
     * 
     * @param controller O controller a ser definido.
     */
    public void setController(PersistenceEnabledController controller) {
        this.controller = SpringBeanReference.forBean(controller);
    }

    /**
     * Testa se o objeto está dirty.
     * 
     * @return <code>true</code> caso esteja, <code>false</code> caso contrário.
     */
    @Transient
    public boolean isDirty() {
        return this.dirty;
    }

    /**
     * Define se o objeto está dirty ou não.
     * 
     * @param flag indicando se o objeto está dirty ou não.
     */
    public void setDirty(boolean flag) {
        this.dirty = flag;
    }

    /**
     * Retorna uma instância do log para uma determinada instância de classe.
     * 
     * @return logger.
     */
    @Transient
    protected Logger getLogger() {
        return Logger.getLogger(this.getClass());
    }

    /**
     * Comparador default para objetos persistíveis.
     * <P>
     * Verifica se a PK está preenchida. Se esitiver, compara com o valor da PK do objeto sendo comparado.
     * 
     * @param obj o objeto sendo comparado.
     * @return true se for do mesmo tipo e igual.
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        Class<?> thisType = HibernateProxyHelper.getClassWithoutInitializingProxy(this);
        Class<?> otherType = HibernateProxyHelper.getClassWithoutInitializingProxy(obj);
        if (!thisType.isAssignableFrom(otherType) && !otherType.isAssignableFrom(thisType)) {
            return false;
        }
        PersistableBusinessObject other = (PersistableBusinessObject) obj;
        if (this.getPk() == null || other.getPk() == null) {
            return false;
        }
        return this.getPk().equals(other.getPk());
    }

    /**
     * Compara utilizando a PK.
     * 
     * @return o valor do hashCode.
     */
    @Override
    public int hashCode() {
        if (this.getPk() == null) {
            return super.hashCode();
        } else {
            return this.getPk().hashCode();
        }
    }

    /**
     * Implementação que retorna o toString() da pk.
     * 
     * @return o toString() da pk.
     */
    public String identification() {
        return this.getPk().toString();
    }
}